// <file>
//     <copyright see="prj:///doc/copyright.txt"/>
//     <license see="prj:///doc/license.txt"/>
//     <author name="Daniel Grunwald"/>
//     <version>$Revision: 5747 $</version>
// </file>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Indentation;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit.Editing
{
    /// <summary>
    /// Control that wraps a TextView and adds support for user input and the caret.
    /// </summary>
    public class TextArea : Control, IScrollInfo, IWeakEventListener, ITextEditorComponent
    {
        #region Constructor

        static TextArea()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (TextArea),
                                                     new FrameworkPropertyMetadata(typeof (TextArea)));
            KeyboardNavigation.IsTabStopProperty.OverrideMetadata(
                typeof (TextArea), new FrameworkPropertyMetadata(Boxes.True));
            KeyboardNavigation.TabNavigationProperty.OverrideMetadata(
                typeof (TextArea), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
            FocusableProperty.OverrideMetadata(
                typeof (TextArea), new FrameworkPropertyMetadata(Boxes.True));
        }

        /// <summary>
        /// Creates a new TextArea instance.
        /// </summary>
        public TextArea() : this(new TextView())
        {
        }

        /// <summary>
        /// Creates a new TextArea instance.
        /// </summary>
        protected TextArea(TextView textView)
        {
            if (textView == null)
                throw new ArgumentNullException("textView");
            _textView = textView;
            Options = textView.Options;

            textView.Services.AddService(typeof (TextArea), this);

            textView.LineTransformers.Add(new SelectionColorizer(this));
            textView.InsertLayer(new SelectionLayer(this), KnownLayer.Selection, LayerInsertionPosition.Replace);

            _caret = new Caret(this);
            _caret.PositionChanged += (sender, e) => RequestSelectionValidation();

            _leftMargins.CollectionChanged += LeftMarginsCollectionChanged;

            DefaultInputHandler = new TextAreaDefaultInputHandler(this);
            ActiveInputHandler = DefaultInputHandler;
        }

        #endregion

        #region InputHandler management

        private ITextAreaInputHandler _activeInputHandler;
        private bool _isChangingInputHandler;

        private ImmutableStack<TextAreaStackedInputHandler> _stackedInputHandlers =
            ImmutableStack<TextAreaStackedInputHandler>.Empty;

        /// <summary>
        /// Gets the default input handler.
        /// </summary>
        /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks>
        public TextAreaDefaultInputHandler DefaultInputHandler { get; private set; }

        /// <summary>
        /// Gets/Sets the active input handler.
        /// This property does not return currently active stacked input handlers. Setting this property detached all stacked input handlers.
        /// </summary>
        /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks>
        public ITextAreaInputHandler ActiveInputHandler
        {
            get { return _activeInputHandler; }
            set
            {
                if (value != null && value.TextArea != this)
                    throw new ArgumentException("The input handler was created for a different text area than this one.");
                if (_isChangingInputHandler)
                    throw new InvalidOperationException("Cannot set ActiveInputHandler recursively");
                if (_activeInputHandler != value)
                {
                    _isChangingInputHandler = true;
                    try
                    {
                        // pop the whole stack
                        PopStackedInputHandler(_stackedInputHandlers.LastOrDefault());
                        Debug.Assert(_stackedInputHandlers.IsEmpty);

                        if (_activeInputHandler != null)
                            _activeInputHandler.Detach();
                        _activeInputHandler = value;
                        if (value != null)
                            value.Attach();
                    }
                    finally
                    {
                        _isChangingInputHandler = false;
                    }
                    if (ActiveInputHandlerChanged != null)
                        ActiveInputHandlerChanged(this, EventArgs.Empty);
                }
            }
        }

        /// <summary>
        /// Gets the list of currently active stacked input handlers.
        /// </summary>
        /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks>
        public ImmutableStack<TextAreaStackedInputHandler> StackedInputHandlers
        {
            get { return _stackedInputHandlers; }
        }

        /// <summary>
        /// Occurs when the ActiveInputHandler property changes.
        /// </summary>
        public event EventHandler ActiveInputHandlerChanged;

        /// <summary>
        /// Pushes an input handler onto the list of stacked input handlers.
        /// </summary>
        /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks>
        public void PushStackedInputHandler(TextAreaStackedInputHandler inputHandler)
        {
            if (inputHandler == null)
                throw new ArgumentNullException("inputHandler");
            _stackedInputHandlers = _stackedInputHandlers.Push(inputHandler);
            inputHandler.Attach();
        }

        /// <summary>
        /// Pops the stacked input handler (and all input handlers above it).
        /// If <paramref name="inputHandler"/> is not found in the currently stacked input handlers, or is null, this method
        /// does nothing.
        /// </summary>
        /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks>
        public void PopStackedInputHandler(TextAreaStackedInputHandler inputHandler)
        {
            if (_stackedInputHandlers.Any(i => i == inputHandler))
            {
                ITextAreaInputHandler oldHandler;
                do
                {
                    oldHandler = _stackedInputHandlers.Peek();
                    _stackedInputHandlers = _stackedInputHandlers.Pop();
                    oldHandler.Detach();
                } while (oldHandler != inputHandler);
            }
        }

        #endregion

        #region Document property

        /// <summary>
        /// Document property.
        /// </summary>
        public static readonly DependencyProperty DocumentProperty
            = TextView.DocumentProperty.AddOwner(typeof (TextArea), new FrameworkPropertyMetadata(OnDocumentChanged));

        /// <summary>
        /// Gets/Sets the document displayed by the text editor.
        /// </summary>
        public TextDocument Document
        {
            get { return (TextDocument) GetValue(DocumentProperty); }
            set { SetValue(DocumentProperty, value); }
        }

        /// <inheritdoc/>
        public event EventHandler DocumentChanged;

        private static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            ((TextArea) dp).OnDocumentChanged((TextDocument) e.OldValue, (TextDocument) e.NewValue);
        }

        private void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
        {
            if (oldValue != null)
            {
                TextDocumentWeakEventManager.Changing.RemoveListener(oldValue, this);
                TextDocumentWeakEventManager.Changed.RemoveListener(oldValue, this);
                TextDocumentWeakEventManager.UpdateStarted.RemoveListener(oldValue, this);
                TextDocumentWeakEventManager.UpdateFinished.RemoveListener(oldValue, this);
            }
            _textView.Document = newValue;
            if (newValue != null)
            {
                TextDocumentWeakEventManager.Changing.AddListener(newValue, this);
                TextDocumentWeakEventManager.Changed.AddListener(newValue, this);
                TextDocumentWeakEventManager.UpdateStarted.AddListener(newValue, this);
                TextDocumentWeakEventManager.UpdateFinished.AddListener(newValue, this);
            }
            // Reset caret location and selection: this is necessary because the caret/selection might be invalid
            // in the new document (e.g. if new document is shorter than the old document).
            _caret.Location = new TextLocation(1, 1);
            Selection = Selection.Empty;
            if (DocumentChanged != null)
                DocumentChanged(this, EventArgs.Empty);
            CommandManager.InvalidateRequerySuggested();
        }

        #endregion

        #region Options property

        /// <summary>
        /// Options property.
        /// </summary>
        public static readonly DependencyProperty OptionsProperty
            = TextView.OptionsProperty.AddOwner(typeof (TextArea), new FrameworkPropertyMetadata(OnOptionsChanged));

        /// <summary>
        /// Gets/Sets the document displayed by the text editor.
        /// </summary>
        public TextEditorOptions Options
        {
            get { return (TextEditorOptions) GetValue(OptionsProperty); }
            set { SetValue(OptionsProperty, value); }
        }

        /// <summary>
        /// Occurs when a text editor option has changed.
        /// </summary>
        public event PropertyChangedEventHandler OptionChanged;

        /// <summary>
        /// Raises the <see cref="OptionChanged"/> event.
        /// </summary>
        protected virtual void OnOptionChanged(PropertyChangedEventArgs e)
        {
            if (OptionChanged != null)
            {
                OptionChanged(this, e);
            }
        }

        private static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            ((TextArea) dp).OnOptionsChanged((TextEditorOptions) e.OldValue, (TextEditorOptions) e.NewValue);
        }

        private void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue)
        {
            if (oldValue != null)
            {
                PropertyChangedWeakEventManager.RemoveListener(oldValue, this);
            }
            _textView.Options = newValue;
            if (newValue != null)
            {
                PropertyChangedWeakEventManager.AddListener(newValue, this);
            }
            OnOptionChanged(new PropertyChangedEventArgs(null));
        }

        #endregion

        #region ReceiveWeakEvent

        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            return ReceiveWeakEvent(managerType, sender, e);
        }

        /// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
        protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            if (managerType == typeof (TextDocumentWeakEventManager.Changing))
            {
                OnDocumentChanging();
                return true;
            }
            if (managerType == typeof (TextDocumentWeakEventManager.Changed))
            {
                OnDocumentChanged((DocumentChangeEventArgs) e);
                return true;
            }
            if (managerType == typeof (TextDocumentWeakEventManager.UpdateStarted))
            {
                OnUpdateStarted();
                return true;
            }
            if (managerType == typeof (TextDocumentWeakEventManager.UpdateFinished))
            {
                OnUpdateFinished();
                return true;
            }
            if (managerType == typeof (PropertyChangedWeakEventManager))
            {
                OnOptionChanged((PropertyChangedEventArgs) e);
                return true;
            }
            return false;
        }

        #endregion

        #region Caret handling on document changes

        private void OnDocumentChanging()
        {
            _caret.OnDocumentChanging();
        }

        private void OnDocumentChanged(DocumentChangeEventArgs e)
        {
            _caret.OnDocumentChanged(e);
            Selection = _selection.UpdateOnDocumentChange(e);
        }

        private void OnUpdateStarted()
        {
            Document.UndoStack.PushOptional(new RestoreCaretAndSelectionUndoAction(this));
        }

        private void OnUpdateFinished()
        {
            _caret.OnDocumentUpdateFinished();
        }

        private sealed class RestoreCaretAndSelectionUndoAction : IUndoableOperation
        {
            // keep textarea in weak reference because the IUndoableOperation is stored with the document
            private readonly TextViewPosition _caretPosition;
            private readonly Selection _selection;
            private readonly WeakReference _textAreaReference;

            public RestoreCaretAndSelectionUndoAction(TextArea textArea)
            {
                _textAreaReference = new WeakReference(textArea);
                _caretPosition = textArea.Caret.Position;
                _selection = textArea.Selection;
            }

            #region IUndoableOperation Members

            public void Undo()
            {
                var textArea = (TextArea) _textAreaReference.Target;
                if (textArea != null)
                {
                    textArea.Caret.Position = _caretPosition;
                    textArea.Selection = _selection;
                }
            }

            public void Redo()
            {
                // redo=undo: we just restore the caret/selection state
                Undo();
            }

            #endregion
        }

        #endregion

        #region TextView property

        private readonly TextView _textView;
        private IScrollInfo _scrollInfo;

        /// <summary>
        /// Gets the text view used to display text in this text area.
        /// </summary>
        public TextView TextView
        {
            get { return _textView; }
        }

        /// <inheritdoc/>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _scrollInfo = _textView;
            ApplyScrollInfo();
        }

        #endregion

        #region Selection property

        /// <summary>
        /// The <see cref="SelectionBrush"/> property.
        /// </summary>
        public static readonly DependencyProperty SelectionBrushProperty =
            DependencyProperty.Register("SelectionBrush", typeof (Brush), typeof (TextArea));

        /// <summary>
        /// The <see cref="SelectionForeground"/> property.
        /// </summary>
        public static readonly DependencyProperty SelectionForegroundProperty =
            DependencyProperty.Register("SelectionForeground", typeof (Brush), typeof (TextArea));

        /// <summary>
        /// The <see cref="SelectionBorder"/> property.
        /// </summary>
        public static readonly DependencyProperty SelectionBorderProperty =
            DependencyProperty.Register("SelectionBorder", typeof (Pen), typeof (TextArea));

        /// <summary>
        /// The <see cref="SelectionCornerRadius"/> property.
        /// </summary>
        public static readonly DependencyProperty SelectionCornerRadiusProperty =
            DependencyProperty.Register("SelectionCornerRadius", typeof (double), typeof (TextArea),
                                        new FrameworkPropertyMetadata(3.0));

        private Selection _selection = Selection.Empty;

        /// <summary>
        /// Gets/Sets the selection in this text area.
        /// </summary>
        public Selection Selection
        {
            get { return _selection; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");
                if (!Equals(_selection, value))
                {
                    Debug.WriteLine("Selection change from " + _selection + " to " + value);
                    if (_textView != null)
                    {
                        _textView.Redraw(_selection.SurroundingSegment, DispatcherPriority.Background);
                        _textView.Redraw(value.SurroundingSegment, DispatcherPriority.Background);
                    }
                    _selection = value;
                    if (SelectionChanged != null)
                        SelectionChanged(this, EventArgs.Empty);
                    // a selection change causes commands like copy/paste/etc. to change status
                    CommandManager.InvalidateRequerySuggested();
                }
            }
        }

        /// <summary>
        /// Gets/Sets the background brush used for the selection.
        /// </summary>
        public Brush SelectionBrush
        {
            get { return (Brush) GetValue(SelectionBrushProperty); }
            set { SetValue(SelectionBrushProperty, value); }
        }

        /// <summary>
        /// Gets/Sets the foreground brush used selected text.
        /// </summary>
        public Brush SelectionForeground
        {
            get { return (Brush) GetValue(SelectionForegroundProperty); }
            set { SetValue(SelectionForegroundProperty, value); }
        }

        /// <summary>
        /// Gets/Sets the background brush used for the selection.
        /// </summary>
        public Pen SelectionBorder
        {
            get { return (Pen) GetValue(SelectionBorderProperty); }
            set { SetValue(SelectionBorderProperty, value); }
        }

        /// <summary>
        /// Gets/Sets the corner radius of the selection.
        /// </summary>
        public double SelectionCornerRadius
        {
            get { return (double) GetValue(SelectionCornerRadiusProperty); }
            set { SetValue(SelectionCornerRadiusProperty, value); }
        }

        /// <summary>
        /// Occurs when the selection has changed.
        /// </summary>
        public event EventHandler SelectionChanged;

        #endregion

        #region Force caret to stay inside selection

        private int _allowCaretOutsideSelection;
        private bool _ensureSelectionValidRequested;

        private void RequestSelectionValidation()
        {
            if (!_ensureSelectionValidRequested && _allowCaretOutsideSelection == 0)
            {
                _ensureSelectionValidRequested = true;
                Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(EnsureSelectionValid));
            }
        }

        /// <summary>
        /// Code that updates only the caret but not the selection can cause confusion when
        /// keys like 'Delete' delete the (possibly invisible) selected text and not the
        /// text around the caret.
        /// 
        /// So we'll ensure that the caret is inside the selection.
        /// (when the caret is not in the selection, we'll clear the selection)
        /// 
        /// This method is invoked using the Dispatcher so that code may temporarily violate this rule
        /// (e.g. most 'extend selection' methods work by first setting the caret, then the selection),
        /// it's sufficient to fix it after any event handlers have run.
        /// </summary>
        private void EnsureSelectionValid()
        {
            _ensureSelectionValidRequested = false;
            if (_allowCaretOutsideSelection == 0)
            {
                if (!_selection.IsEmpty && !_selection.Contains(_caret.Offset))
                {
                    Debug.WriteLine("Resetting selection because caret is outside");
                    Selection = Selection.Empty;
                }
            }
        }

        /// <summary>
        /// Temporarily allows positioning the caret outside the selection.
        /// Dispose the returned IDisposable to revert the allowance.
        /// </summary>
        /// <remarks>
        /// The text area only forces the caret to be inside the selection when other events
        /// have finished running (using the dispatcher), so you don't have to use this method
        /// for temporarily positioning the caret in event handlers.
        /// This method is only necessary if you want to run the WPF dispatcher, e.g. if you
        /// perform a drag'n'drop operation.
        /// </remarks>
        public IDisposable AllowCaretOutsideSelection()
        {
            VerifyAccess();
            _allowCaretOutsideSelection++;
            return new CallbackOnDispose(
                delegate
                    {
                        VerifyAccess();
                        _allowCaretOutsideSelection--;
                        RequestSelectionValidation();
                    });
        }

        #endregion

        #region Properties

        private readonly Caret _caret;

        private readonly ObservableCollection<UIElement> _leftMargins = new ObservableCollection<UIElement>();

        private IReadOnlySectionProvider _readOnlySectionProvider = NoReadOnlySections.Instance;

        /// <summary>
        /// Gets the Caret used for this text area.
        /// </summary>
        public Caret Caret
        {
            get { return _caret; }
        }

        /// <summary>
        /// Gets the collection of margins displayed to the left of the text view.
        /// </summary>
        public ObservableCollection<UIElement> LeftMargins
        {
            get { return _leftMargins; }
        }

        /// <summary>
        /// Gets/Sets an object that provides read-only sections for the text area.
        /// </summary>
        public IReadOnlySectionProvider ReadOnlySectionProvider
        {
            get { return _readOnlySectionProvider; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");
                _readOnlySectionProvider = value;
            }
        }

        private void LeftMarginsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.OldItems != null)
            {
                foreach (ITextViewConnect c in e.OldItems.OfType<ITextViewConnect>())
                {
                    c.RemoveFromTextView(_textView);
                }
            }
            if (e.NewItems != null)
            {
                foreach (ITextViewConnect c in e.NewItems.OfType<ITextViewConnect>())
                {
                    c.AddToTextView(_textView);
                }
            }
        }

        #endregion

        #region IScrollInfo implementation

        private bool _canHorizontallyScroll;
        private bool _canVerticallyScroll;
        private ScrollViewer _scrollOwner;

        bool IScrollInfo.CanVerticallyScroll
        {
            get { return _scrollInfo != null ? _scrollInfo.CanVerticallyScroll : false; }
            set
            {
                _canVerticallyScroll = value;
                if (_scrollInfo != null)
                    _scrollInfo.CanVerticallyScroll = value;
            }
        }

        bool IScrollInfo.CanHorizontallyScroll
        {
            get { return _scrollInfo != null ? _scrollInfo.CanHorizontallyScroll : false; }
            set
            {
                _canHorizontallyScroll = value;
                if (_scrollInfo != null)
                    _scrollInfo.CanHorizontallyScroll = value;
            }
        }

        double IScrollInfo.ExtentWidth
        {
            get { return _scrollInfo != null ? _scrollInfo.ExtentWidth : 0; }
        }

        double IScrollInfo.ExtentHeight
        {
            get { return _scrollInfo != null ? _scrollInfo.ExtentHeight : 0; }
        }

        double IScrollInfo.ViewportWidth
        {
            get { return _scrollInfo != null ? _scrollInfo.ViewportWidth : 0; }
        }

        double IScrollInfo.ViewportHeight
        {
            get { return _scrollInfo != null ? _scrollInfo.ViewportHeight : 0; }
        }

        double IScrollInfo.HorizontalOffset
        {
            get { return _scrollInfo != null ? _scrollInfo.HorizontalOffset : 0; }
        }

        double IScrollInfo.VerticalOffset
        {
            get { return _scrollInfo != null ? _scrollInfo.VerticalOffset : 0; }
        }

        ScrollViewer IScrollInfo.ScrollOwner
        {
            get { return _scrollInfo != null ? _scrollInfo.ScrollOwner : null; }
            set
            {
                if (_scrollInfo != null)
                    _scrollInfo.ScrollOwner = value;
                else
                    _scrollOwner = value;
            }
        }

        void IScrollInfo.LineUp()
        {
            if (_scrollInfo != null) _scrollInfo.LineUp();
        }

        void IScrollInfo.LineDown()
        {
            if (_scrollInfo != null) _scrollInfo.LineDown();
        }

        void IScrollInfo.LineLeft()
        {
            if (_scrollInfo != null) _scrollInfo.LineLeft();
        }

        void IScrollInfo.LineRight()
        {
            if (_scrollInfo != null) _scrollInfo.LineRight();
        }

        void IScrollInfo.PageUp()
        {
            if (_scrollInfo != null) _scrollInfo.PageUp();
        }

        void IScrollInfo.PageDown()
        {
            if (_scrollInfo != null) _scrollInfo.PageDown();
        }

        void IScrollInfo.PageLeft()
        {
            if (_scrollInfo != null) _scrollInfo.PageLeft();
        }

        void IScrollInfo.PageRight()
        {
            if (_scrollInfo != null) _scrollInfo.PageRight();
        }

        void IScrollInfo.MouseWheelUp()
        {
            if (_scrollInfo != null) _scrollInfo.MouseWheelUp();
        }

        void IScrollInfo.MouseWheelDown()
        {
            if (_scrollInfo != null) _scrollInfo.MouseWheelDown();
        }

        void IScrollInfo.MouseWheelLeft()
        {
            if (_scrollInfo != null) _scrollInfo.MouseWheelLeft();
        }

        void IScrollInfo.MouseWheelRight()
        {
            if (_scrollInfo != null) _scrollInfo.MouseWheelRight();
        }

        void IScrollInfo.SetHorizontalOffset(double offset)
        {
            if (_scrollInfo != null) _scrollInfo.SetHorizontalOffset(offset);
        }

        void IScrollInfo.SetVerticalOffset(double offset)
        {
            if (_scrollInfo != null) _scrollInfo.SetVerticalOffset(offset);
        }

        Rect IScrollInfo.MakeVisible(Visual visual, Rect rectangle)
        {
            return _scrollInfo != null ? _scrollInfo.MakeVisible(visual, rectangle) : Rect.Empty;
        }

        private void ApplyScrollInfo()
        {
            if (_scrollInfo != null)
            {
                _scrollInfo.ScrollOwner = _scrollOwner;
                _scrollInfo.CanVerticallyScroll = _canVerticallyScroll;
                _scrollInfo.CanHorizontallyScroll = _canHorizontallyScroll;
                _scrollOwner = null;
            }
        }

        #endregion

        #region Focus Handling (Show/Hide Caret)

        ///<summary>
        ///</summary>
        public bool DisableCaretHide { get; set; }

        /// <inheritdoc/>
        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            base.OnMouseDown(e);
            Focus();
        }

        /// <inheritdoc/>
        protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            base.OnGotKeyboardFocus(e);
            _caret.Show();
        }

        /// <inheritdoc/>
        protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            base.OnLostKeyboardFocus(e);
            if (!DisableCaretHide) _caret.Hide();
        }

        #endregion

        #region OnTextInput / RemoveSelectedText / ReplaceSelectionWithText

        /// <summary>
        /// Occurs when the TextArea receives text input.
        /// This is like the <see cref="UIElement.TextInput"/> event,
        /// but occurs immediately before the TextArea handles the TextInput event.
        /// </summary>
        public event TextCompositionEventHandler TextEntering;

        /// <summary>
        /// Occurs when the TextArea receives text input.
        /// This is like the <see cref="UIElement.TextInput"/> event,
        /// but occurs immediately after the TextArea handles the TextInput event.
        /// </summary>
        public event TextCompositionEventHandler TextEntered;

        /// <summary>
        /// Raises the TextEntering event.
        /// </summary>
        protected virtual void OnTextEntering(TextCompositionEventArgs e)
        {
            if (TextEntering != null)
            {
                TextEntering(this, e);
            }
        }

        /// <summary>
        /// Raises the TextEntered event.
        /// </summary>
        protected virtual void OnTextEntered(TextCompositionEventArgs e)
        {
            if (TextEntered != null)
            {
                TextEntered(this, e);
            }
        }

        /// <inheritdoc/>
        protected override void OnTextInput(TextCompositionEventArgs e)
        {
            //Debug.WriteLine("TextInput: Text='" + e.Text + "' SystemText='" + e.SystemText + "' ControlText='" + e.ControlText + "'");
            base.OnTextInput(e);
            if (!e.Handled && Document != null)
            {
                if (string.IsNullOrEmpty(e.Text) || e.Text == "\x1b")
                {
                    // ASCII 0x1b = ESC.
                    // WPF produces a TextInput event with that old ASCII control char
                    // when Escape is pressed. We'll just ignore it.

                    // Similarly, some shortcuts like Alt+Space produce an empty TextInput event.
                    // We have to ignore those (not handle them) to keep the shortcut working.
                    return;
                }
                PerformTextInput(e);
                e.Handled = true;
            }
        }

        /// <summary>
        /// Performs text input.
        /// This raises the <see cref="TextEntering"/> event, replaces the selection with the text,
        /// and then raises the <see cref="TextEntered"/> event.
        /// </summary>
        public void PerformTextInput(string text)
        {
            var textComposition = new TextComposition(InputManager.Current, this, text);
            var e = new TextCompositionEventArgs(Keyboard.PrimaryDevice, textComposition);
            e.RoutedEvent = TextInputEvent;
            PerformTextInput(e);
        }

        /// <summary>
        /// Performs text input.
        /// This raises the <see cref="TextEntering"/> event, replaces the selection with the text,
        /// and then raises the <see cref="TextEntered"/> event.
        /// </summary>
        public void PerformTextInput(TextCompositionEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");
            if (Document == null)
                throw ThrowUtil.NoDocumentAssigned();
            OnTextEntering(e);
            if (!e.Handled)
            {
                if (e.Text == "\n" || e.Text == "\r\n")
                    ReplaceSelectionWithNewLine();
                else
                    ReplaceSelectionWithText(e.Text);
                OnTextEntered(e);
                _caret.BringCaretToView();
            }
        }

        private void ReplaceSelectionWithNewLine()
        {
            string newLine = TextUtilities.GetNewLineFromDocument(Document, Caret.Line);
            using (Document.RunUpdate())
            {
                ReplaceSelectionWithText(newLine);
                if (IndentationStrategy != null)
                {
                    DocumentLine line = Document.GetLineByNumber(Caret.Line);
                    ISegment[] deletable = GetDeletableSegments(line);
                    if (deletable.Length == 1 && deletable[0].Offset == line.Offset &&
                        deletable[0].Length == line.Length)
                    {
                        // use indentation strategy only if the line is not read-only
                        IndentationStrategy.IndentLine(Document, line);
                    }
                }
            }
        }

        internal void RemoveSelectedText()
        {
            if (Document == null)
                throw ThrowUtil.NoDocumentAssigned();
            _selection.ReplaceSelectionWithText(this, string.Empty);
#if DEBUG
            if (!_selection.IsEmpty)
            {
                foreach (ISegment s in _selection.Segments)
                {
                    Debug.Assert(ReadOnlySectionProvider.GetDeletableSegments(s).Count() == 0);
                }
            }
#endif
        }

        internal void ReplaceSelectionWithText(string newText)
        {
            if (newText == null)
                throw new ArgumentNullException("newText");
            if (Document == null)
                throw ThrowUtil.NoDocumentAssigned();
            _selection.ReplaceSelectionWithText(this, newText);
        }

        internal ISegment[] GetDeletableSegments(ISegment segment)
        {
            IEnumerable<ISegment> deletableSegments = ReadOnlySectionProvider.GetDeletableSegments(segment);
            if (deletableSegments == null)
                throw new InvalidOperationException("ReadOnlySectionProvider.GetDeletableSegments returned null");
            ISegment[] array = deletableSegments.ToArray();
            int lastIndex = segment.Offset;
            for (int i = 0; i < array.Length; i++)
            {
                if (array[i].Offset < lastIndex)
                    throw new InvalidOperationException(
                        "ReadOnlySectionProvider returned incorrect segments (outside of input segment / wrong order)");
                lastIndex = array[i].EndOffset;
            }
            if (lastIndex > segment.EndOffset)
                throw new InvalidOperationException(
                    "ReadOnlySectionProvider returned incorrect segments (outside of input segment / wrong order)");
            return array;
        }

        #endregion

        #region IndentationStrategy property

        /// <summary>
        /// IndentationStrategy property.
        /// </summary>
        public static readonly DependencyProperty IndentationStrategyProperty =
            DependencyProperty.Register("IndentationStrategy", typeof (IIndentationStrategy), typeof (TextArea),
                                        new FrameworkPropertyMetadata(new DefaultIndentationStrategy()));

        /// <summary>
        /// Gets/Sets the indentation strategy used when inserting new lines.
        /// </summary>
        public IIndentationStrategy IndentationStrategy
        {
            get { return (IIndentationStrategy) GetValue(IndentationStrategyProperty); }
            set { SetValue(IndentationStrategyProperty, value); }
        }

        #endregion

        #region ITextEditorComponent Members

        /// <summary>
        /// Gets the requested service.
        /// </summary>
        /// <returns>Returns the requested service instance, or null if the service cannot be found.</returns>
        public virtual object GetService(Type serviceType)
        {
            return _textView.Services.GetService(serviceType);
        }

        #endregion

        /// <inheritdoc/>
        protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
        {
            // accept clicks even where the text area draws no background
            return new PointHitTestResult(this, hitTestParameters.HitPoint);
        }

        /// <inheritdoc/>
        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);
            if (e.Property == SelectionBrushProperty
                || e.Property == SelectionBorderProperty
                || e.Property == SelectionForegroundProperty
                || e.Property == SelectionCornerRadiusProperty)
            {
                _textView.Redraw();
            }
        }

        /// <summary>
        /// Occurs when text inside the TextArea was copied.
        /// </summary>
        public event EventHandler<TextEventArgs> TextCopied;

        internal void OnTextCopied(TextEventArgs e)
        {
            if (TextCopied != null)
                TextCopied(this, e);
        }

        #region OnKeyDown/OnKeyUp

        /// <inheritdoc/>
        protected override void OnPreviewKeyDown(KeyEventArgs e)
        {
            base.OnPreviewKeyDown(e);
            foreach (TextAreaStackedInputHandler h in _stackedInputHandlers)
            {
                if (e.Handled)
                    break;
                h.OnPreviewKeyDown(e);
            }
        }

        /// <inheritdoc/>
        protected override void OnPreviewKeyUp(KeyEventArgs e)
        {
            base.OnPreviewKeyUp(e);
            foreach (TextAreaStackedInputHandler h in _stackedInputHandlers)
            {
                if (e.Handled)
                    break;
                h.OnPreviewKeyUp(e);
            }
        }

        // Make life easier for text editor extensions that use a different cursor based on the pressed modifier keys.
        /// <inheritdoc/>
        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);
            TextView.InvalidateCursor();
        }

        /// <inheritdoc/>
        protected override void OnKeyUp(KeyEventArgs e)
        {
            base.OnKeyUp(e);
            TextView.InvalidateCursor();
        }

        #endregion
    }

    /// <summary>
    /// EventArgs with text.
    /// </summary>
    [Serializable]
    public class TextEventArgs : EventArgs
    {
        private readonly string _text;

        /// <summary>
        /// Creates a new TextEventArgs instance.
        /// </summary>
        public TextEventArgs(string text)
        {
            if (text == null)
                throw new ArgumentNullException("text");
            _text = text;
        }

        /// <summary>
        /// Gets the text.
        /// </summary>
        public string Text
        {
            get { return _text; }
        }
    }
}